<?php
 
 
namespace snowflake;
 
 
use think\Exception;
 
class SnowFlake
{
    const TWEPOCH = 1288834974657; // 时间起始标记点，作为基准，一般取系统的最近时间（一旦确定不能变动）
 
    const WORKER_ID_BITS = 5; // 机器标识位数
    const DATACENTER_ID_BITS = 5; // 数据中心标识位数
    const SEQUENCE_BITS = 12; // 毫秒内自增位
 
    private $workerId = 5; // 工作机器ID(0~31)这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数
    private $datacenterId = 5; // 数据中心ID(0~31)支持的最大数据标识id
    private $sequence = 0; // 毫秒内序列(0~4095) 序列在id中占的位数
 
    private $maxWorkerId = -1 ^ (-1 << self::WORKER_ID_BITS); // 机器ID最大值31
    private $maxDatacenterId = -1 ^ (-1 << self::DATACENTER_ID_BITS); // 数据中心ID最大值31
 
    private $workerIdShift = self::SEQUENCE_BITS; // 机器ID偏左移12位
    private $datacenterIdShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS; // 数据中心ID左移17位
    private $timestampLeftShift = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS; // 时间毫秒左移22位
    private $sequenceMask = -1 ^ (-1 << self::SEQUENCE_BITS); // 生成序列的掩码4095
 
    private $lastTimestamp = -1; // 上次生产id时间戳
 
 
    /**
     * 生产唯一id号
     * @return string
     * @throws \Exception
     */
    public function nextId()
    {
        $timestamp = $this->timeGen();
 
        if ($timestamp < $this->lastTimestamp) {
            $diffTimestamp = bcsub($this->lastTimestamp, $timestamp);
            throw new \Exception("Clock moved backwards.  Refusing to generate id for {$diffTimestamp} milliseconds");
        }
 
        if ($this->lastTimestamp == $timestamp) {
            $this->sequence = ($this->sequence + 1) & $this->sequenceMask;
 
            if (0 == $this->sequence) {
                $timestamp = $this->tilNextMillis($this->lastTimestamp);
            }
        } else {
            $this->sequence = 0;
        }
        $this->lastTimestamp = $timestamp;
        $gmpTimestamp = gmp_init($this->leftShift(bcsub($timestamp, self::TWEPOCH), $this->timestampLeftShift));
        $gmpDatacenterId = gmp_init($this->leftShift($this->datacenterId, $this->datacenterIdShift));
        $gmpWorkerId = gmp_init($this->leftShift($this->workerId, $this->workerIdShift));
        $gmpSequence = gmp_init($this->sequence);
        return gmp_strval(gmp_or(gmp_or(gmp_or($gmpTimestamp, $gmpDatacenterId), $gmpWorkerId), $gmpSequence));
    }
 
    /**
     * @param $lastTimestamp
     * @return false|float
     */
    protected function tilNextMillis($lastTimestamp)
    {
        $timestamp = $this->timeGen();
        while ($timestamp <= $lastTimestamp) {
            $timestamp = $this->timeGen();
        }
        return $timestamp;
    }
 
    /**
     * @return false|float
     */
    protected function timeGen()
    {
        return floor(microtime(true) * 1000);
    }
 
    /**
     * 左移 <<
     * @param $a
     * @param $b
     * @return string
     */
    protected function leftShift($a, $b)
    {
        return bcmul($a, bcpow(2, $b));
    }
 
 
}